Skip to content

Add relationship-options read primitive + serverAction op#636

Merged
borisno2 merged 1 commit into
mainfrom
claude/issue-630-nop1jf
Jul 10, 2026
Merged

Add relationship-options read primitive + serverAction op#636
borisno2 merged 1 commit into
mainfrom
claude/issue-630-nop1jf

Conversation

@borisno2

Copy link
Copy Markdown
Member

Summary

  • Add getRelationshipOptions(context, config, relatedListKey, { search?, take?, selectedIds? }) in @opensaas/stack-core (packages/core/src/query/relationship-options.ts) — a bounded, projected { id, label }[] read primitive for relationship editors.
    • Builds a scalar-only fragment ({ id: true, [labelField]: true }, label field from getLabelFieldName) and reads via runQuery/the existing context.db.* fragment path, so buildIncludeWithAccessControl's depth-5 auto-include never runs for this fetch.
    • where: contains on the label field when it's a text field; otherwise no filter (first-N).
    • orderBy: label field ascending. take: bounded window (default 50).
    • Always unions the currently-selected id(s) via a second { id: { in: selectedIds } } query so the caller can render the current value even when it falls outside the search/take window.
    • Maps rows through getItemLabel. Related-list query access denial still applies (falls through to []).
  • Add a relationshipOptions op on context.serverAction ({ listKey, action: 'relationshipOptions', field, search?, take?, selectedIds? }) that resolves the field's ref to the related list and calls the primitive — one wiring point, one access boundary.
  • Re-export getRelationshipOptions from @opensaas/stack-ui for server components that already hold a context (e.g. alongside prepareItemForm). No UI consumers change in this slice, per the issue.
  • Changeset added (@opensaas/stack-core + @opensaas/stack-ui: minor).

Test plan

  • Unit tests for getRelationshipOptions (packages/core/src/query/relationship-options.test.ts): scalar-only fragment / no nested include, take bound, search contains + orderBy on a text label field, no filter on a non-text label field, selected-id union beyond the window, no re-query when already in window, [] on denied access, [] for an unknown related list.
  • serverAction integration tests (packages/core/tests/context.test.ts): options via the op, take bound, selected-id union, [] data on related-list access denial, error on a non-relationship / missing field.
  • stack-ui re-export test (packages/ui/tests/lib/getRelationshipOptions.test.ts).
  • pnpm lint, pnpm manypkg fix, pnpm format
  • pnpm build (all 11 packages)
  • Full test suite across core, ui, auth, cli, rag, storage, storage-s3, storage-vercel — all green

Closes #630


Generated by Claude Code

Adds getRelationshipOptions(context, config, relatedListKey, { search?,
take?, selectedIds? }) — a bounded, projected { id, label }[] fetch for
relationship editors. It uses a scalar-only fragment (id + resolved label
field) so buildIncludeWithAccessControl's depth-5 auto-include never runs,
filters via `contains` on the label field when it's text, orders by that
field, and always unions currently-selected ids beyond the take/search
window. Exposed through a new `relationshipOptions` op on
context.serverAction so hosts have one wiring point and one access
boundary. Re-exported from @opensaas/stack-ui for server components that
already hold a context.

Closes #630

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeeiV2U73LJyS8MEZHcWE8
@changeset-bot

changeset-bot Bot commented Jul 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0f715da

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@opensaas/stack-core Minor
@opensaas/stack-ui Minor
@opensaas/stack-auth Minor
@opensaas/stack-cli Minor
@opensaas/stack-rag Minor
@opensaas/stack-storage Minor
@opensaas/stack-tiptap Minor
@opensaas/stack-storage-s3 Minor
@opensaas/stack-storage-vercel Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jul 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stack-docs Ready Ready Preview, Comment Jul 10, 2026 7:52am

Copy link
Copy Markdown
Member Author

Self-review

Overview

Adds getRelationshipOptions(context, config, relatedListKey, { search?, take?, selectedIds? }) (packages/core/src/query/relationship-options.ts) — a bounded, projected { id, label }[] read for relationship editors, built on the existing scalar-only fragment path (defineFragment/runQuery) so buildIncludeWithAccessControl's depth-5 auto-include never runs. Wires it into context.serverAction as a new relationshipOptions op, and re-exports it from @opensaas/stack-ui.

Correctness

  • Verified filterReadableFields still runs unconditionally after the fragment-path findMany (context/index.ts:871-887), independent of the buildIncludeWithAccessControl bypass — so field-level read access on the label field is still enforced even though relation-level access-scoping is skipped for this scalar-only fetch. If the label field is stripped, getItemLabel falls back to id, matching the label-seam contract from Label seam in stack-core (getLabelFieldName / getItemLabel + ui.labelField) #629.
  • Operation-level query access on the related list is enforced by the underlying findMany (denied → []), confirmed by both the unit test and the serverAction integration test.
  • Selected-id union correctly skips the second query when all selectedIds are already in the primary window (avoids an unnecessary round trip).
  • labelField === 'id' fallback case collapses the fragment to { id: true } (object literal key dedup) — harmless, no bug.

Code quality / conventions

  • Follows the existing (context, config, ...) parameter convention used by prepareItemForm and other ui-facing helpers.
  • Follows the QueryRunnerContext typing convention already used by runQuery/runQueryOne rather than requiring the full AccessContext.
  • stack-ui's module is a thin re-export of the core implementation (single source of truth, used identically by both the direct-call path and the serverAction op) rather than duplicating the query logic.

Test coverage

  • 8 unit tests on the primitive (scalar-only fragment/no include, take bound, search+orderBy on text label field, no filter on non-text label field, selected-id union beyond window, no redundant query, denied-access → [], unknown list → []).
  • 6 integration tests on the new serverAction op (including access-denial and invalid-field error cases).
  • 2 tests confirming the stack-ui re-export behaves identically against a full AccessContext.
  • pnpm lint, pnpm manypkg fix, pnpm format, and pnpm build all clean; full test suite across all packages (core, ui, auth, cli, rag, storage, storage-s3, storage-vercel) passes.

Risks

  • None identified. The primitive only ever touches a list already reachable via a declared relationship field's ref, and the related list's own operation-level access is always the gate — no new access-control bypass surface.

No changes requested.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for Core Package Coverage (./packages/core)

Status Category Percentage Covered / Total
🔵 Lines 92.22% (🎯 65%) 901 / 977
🔵 Statements 91.56% (🎯 65%) 945 / 1032
🔵 Functions 98.05% (🎯 62%) 151 / 154
🔵 Branches 80.92% (🎯 50%) 615 / 760
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/core/src/query/relationship-options.ts 100% 100% 100% 100%
Generated in workflow #1230 for commit 0f715da by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for UI Package Coverage (./packages/ui)

Status Category Percentage Covered / Total
🔵 Lines 76.03% 92 / 121
🔵 Statements 75.39% 95 / 126
🔵 Functions 75.6% 31 / 41
🔵 Branches 65.78% 75 / 114
File CoverageNo changed files found.
Generated in workflow #1230 for commit 0f715da by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for CLI Package Coverage (./packages/cli)

Status Category Percentage Covered / Total
🔵 Lines 79.21% 1490 / 1881
🔵 Statements 78.92% 1550 / 1964
🔵 Functions 84.45% 201 / 238
🔵 Branches 67.25% 653 / 971
File CoverageNo changed files found.
Generated in workflow #1230 for commit 0f715da by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for Auth Package Coverage (./packages/auth)

Status Category Percentage Covered / Total
🔵 Lines 74.64% 159 / 213
🔵 Statements 69.74% 166 / 238
🔵 Functions 83.11% 64 / 77
🔵 Branches 70.67% 94 / 133
File CoverageNo changed files found.
Generated in workflow #1230 for commit 0f715da by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for Storage Package Coverage (./packages/storage)

Status Category Percentage Covered / Total
🔵 Lines 74.8% 190 / 254
🔵 Statements 76.44% 211 / 276
🔵 Functions 85.89% 67 / 78
🔵 Branches 70.73% 174 / 246
File CoverageNo changed files found.
Generated in workflow #1230 for commit 0f715da by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for RAG Package Coverage (./packages/rag)

Status Category Percentage Covered / Total
🔵 Lines 47.97% 355 / 740
🔵 Statements 48.14% 377 / 783
🔵 Functions 54.26% 70 / 129
🔵 Branches 42.55% 180 / 423
File CoverageNo changed files found.
Generated in workflow #1230 for commit 0f715da by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for Storage S3 Package Coverage (./packages/storage-s3)

Status Category Percentage Covered / Total
🔵 Lines 100% 40 / 40
🔵 Statements 100% 40 / 40
🔵 Functions 100% 9 / 9
🔵 Branches 100% 19 / 19
File CoverageNo changed files found.
Generated in workflow #1230 for commit 0f715da by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for Storage Vercel Package Coverage (./packages/storage-vercel)

Status Category Percentage Covered / Total
🔵 Lines 100% 38 / 38
🔵 Statements 100% 38 / 38
🔵 Functions 100% 8 / 8
🔵 Branches 100% 22 / 22
File CoverageNo changed files found.
Generated in workflow #1230 for commit 0f715da by the Vitest Coverage Report Action

@borisno2 borisno2 merged commit a15e566 into main Jul 10, 2026
6 checks passed
@borisno2 borisno2 deleted the claude/issue-630-nop1jf branch July 10, 2026 08:14
@github-actions github-actions Bot mentioned this pull request Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Relationship-options read primitive + serverAction op (bounded, projected, selected-seeded)

2 participants